#include "ProgramState.hpp"
#include "../FileUtil.hpp"

namespace Program {

std::list<ProgramState *> ProgramState::_programs;

static constexpr GLchar *INTERNAL_UNIFORMS[] = {
    "uniform mat4 PMat;\n",      "uniform mat4 MVMat;\n", "uniform mat4 MVPMat;\n",
    "uniform mat3 NormalMat;\n", "uniform vec4 Time;\n",  "uniform vec4 SinTime;\n",
    "uniform vec4 CosTime;\n",   "// Include End\n"};

static constexpr GLchar *INTERNAL_ATTRIBUTES[] = {"in vec2 vTex;\n", "in vec3 vPos;\n",
                                                  "in vec3 vNor;\n", "in vec3 vBi;\n",
                                                  "in vec3 vTan;\n", "in vec4 vCol;\n"};

std::string ProgramState::GetBuiltInUniformName(ProgramState::BuiltInUniform type) {
  static constexpr GLchar *UNIFORM_NAMS[] = {"PMat", "MVMat",   "MVPMat", "NormalMat",
                                             "Time", "SinTime", "CosTime"};
  return UNIFORM_NAMS[static_cast<size_t>(type)];
}

bool CheckShaderFileNameExt(const std::string &fileName) {
  bool status = false;
  auto i = fileName.find_last_of('.');
  const std::string ext("glsl");
  if (i != std::string::npos) {
    auto extension = fileName.substr(i + 1);
    if (extension.size() != ext.size())
      return status;
    if (extension == ext)
      status = true;
    else
      status = false;
  }
  return status;
}

ShaderType GetShaderTypeFromFileExtension(const std::string &fileName) {
  auto i = fileName.find_last_of('.');
  if (i != std::string::npos) {
    auto extension = fileName.substr(i + 1);
    if (extension[0] == 'v' && extension[1] == 't')
      return ShaderType::VERTEX;
    else if (extension[0] == 'f' && extension[1] == 'g')
      return ShaderType::FRAGMENT;
    else if (extension[0] == 'g' && extension[1] == 'e')
      return ShaderType::GEOMETRY;
    else if (extension[0] == 't' && extension[1] == 'c')
      return ShaderType::TESSLETION_CONTROL;
    else if (extension[0] == 't' && extension[1] == 'e')
      return ShaderType::TESSLETION_EVALUATE;
    else if (extension[0] == 'c' && extension[1] == 't')
      return ShaderType::COMPUTE;
    else
      return ShaderType::ALL;
  } else {
    // TODO: Error extension file name handle
    return ShaderType::ALL;
  }
}

ProgramState::~ProgramState() { Remove(); }

ProgramRef ProgramState::CreatProgram() {
  ProgramState *program = new ProgramState();
  _programs.push_back(program);
  return std::shared_ptr<ProgramState>(program);
}

void ProgramState::RemoveProgram(const ProgramRef &program) {
  _programs.remove_if([&](ProgramState *p) -> bool {
    if (std::shared_ptr<ProgramState>(p) == program) {
      delete p;
      p = nullptr;
      return true;
    }
    return false;
  });
}

void ProgramState::RemoveAll() {
  for (auto &val : _programs) {
    if (val) {
      delete val;
      val = nullptr;
    }
  }
  _programs.clear();
}

// TODO:Add Shader Class Surport
void ProgramState::InitProgram(/* Attach Compiled shader and link program */) {
  _id = glCreateProgram();
}

// whether clear compiled shader object
void ProgramState::LinkProgram(bool clear) {
  glValidateProgram(_id);
  glLinkProgram(_id);
  CheckProgramLinkingError(_id);
  if (clear) {
    for (const auto &val : _shaderId) {
      glDetachShader(_id, val);
      glDeleteShader(val);
    }
    _shaderId.clear();
  }
}

void ProgramState::UseProgram() { glUseProgram(_id); }

void ProgramState::UseProgramStages(GLuint pipeline, ShaderStage stage) {
  if (GetProgramParameter(_id, ProgramParameter::SEPERATE_PROGRAM)) {
    glUseProgram(0);
    glUseProgramStages(pipeline, stage, _id);
  }
}

void ProgramState::UnUse() { glUseProgram(0); }

void ProgramState::BindAttribLocation(GLuint index, const std::string &name) {
  // TODO: May be After linking reset to false
  _bind_loc = true;
  glBindAttribLocation(_id, index, name.c_str());
}

void ProgramState::BindAttributes(const std::vector<Model::VertexAttribFormat> &formats) {
  _bind_loc = true;
  for (size_t i = 0; i < formats.size(); i++) {
    BindAttribLocation(i, Model::GetVertexAttributeName(formats.at(i).type));
  }
}

void ProgramState::BindAllAttributes() {
  _bind_loc = true;
  for (uint8_t i = 0; i < (uint8_t)Model::AttribType::Count; i++) {
    auto type = static_cast<Model::AttribType>(i);
    BindAttribLocation(i, Model::GetVertexAttributeName(type));
  }
}

void ProgramState::BindFragAttribLocation(GLuint index, const std::string &name) const {
  glBindFragDataLocation(_id, index, name.c_str());
}

void ProgramState::GetAttributeLocation(const std::string &name) const {
  glGetAttribLocation(_id, name.c_str());
}

void ProgramState::Remove() {
  for (auto &val : _shaderId) {
    if (glIsShader(val)) {
      glDeleteShader(val);
    }
  }
  if (!_shaderId.empty())
    _shaderId.clear();
  glDeleteProgram(_id);
  _id = 0;
  _bind_loc = false;
}

GLuint ProgramState::GetUniformLocation(const std::string &name) {
  return glGetUniformLocation(_id, name.c_str());
}

void ProgramState::CreateShaderWithByteArrays(const GLchar *shaderSrc, ShaderType type) {
  GLuint shader = CreateShader(type);
  // Can not create this shader may be type error
  if (!glIsShader(shader) || shaderSrc[0] == '\0')
    return;
  CompileShaderFromSrc(shader, shaderSrc);
  glAttachShader(_id, shader);
}

void ProgramState::CreateShaderWithFileName(const std::string &fileName,
                                            ShaderType type) {
  if (!CheckShaderFileNameExt(fileName)) {
    // TODO: shader file extension error handling
#if _DEBUG
    std::cerr << "The shader file extension is not be loaded!" << std::endl;
#endif
    return;
  }
  GLuint shader = CreateShader(type);
  // Can not create this shader may be type error
  if (!glIsShader(shader) || fileName.empty())
    return;
  CompileShaderFromFile(shader, fileName);
  glAttachShader(_id, shader);
}

void ProgramState::CreateShaderWithFileNameExt(const std::string &fileName) {
  auto type = GetShaderTypeFromFileExtension(fileName);
  GLuint shader = CreateShader(type);
  // Can not create this shader may be type error
  if (!glIsShader(shader) || fileName.empty())
    return;
  CompileShaderFromFile(shader, fileName);
  glAttachShader(_id, shader);
}

void ProgramState::UpdateBuiltInUniform(const glm::mat4 &mv) {
  // TODO: Update BuiltIn Uniforms
  auto perspective = glm::perspective(45.f, float(720) / 1280, 0.1f, 10.f);
  SetUniformMat4fArray(GetBuiltInUniformName(BuiltInUniform::UNIFORM_MODELVIEW_MAT), &mv,
                       1);
  SetUniformMat4fArray(GetBuiltInUniformName(BuiltInUniform::UNIFORM_MVP_MAT),
                       &perspective, 1);
}

void ProgramState::CompileShaderFormBin(GLuint id, const GLchar *bytes) {
  // TODO: spir-v shader compile
}

void ProgramState::CompileShaderFromFile(GLuint id, const std::string &fileName) {
  // TODO: use file Phaser
  auto src = Utility::FilePhaser::GetInstance().GetStringFromFile(fileName);
  CompileShaderRaw(id, src.c_str());
}

void ProgramState::CompileShaderFromSrc(GLuint id, const std::string &shaderSrc) {
  CompileShaderRaw(id, shaderSrc.c_str());
}

void ProgramState::CompileShaderRaw(GLuint id, const GLchar *source,
                                    const std::string &headers,
                                    const std::string &defines) {
  // TODO: change this uniform bingding process
  std::string uniformStr;
  for (int i = 0; i < BuiltInUniform::COUNT; i++) {
    uniformStr.append(INTERNAL_UNIFORMS[i]);
  }
  std::string attributeStr;
  for (int i = 0; i < (int)Model::AttribType::Count; i++) {
    attributeStr.append(INTERNAL_ATTRIBUTES[i]);
  }
  GLint param;
  glGetShaderiv(id, GL_SHADER_TYPE, &param);
  const GLchar *sources[] = {defines.c_str(), headers.c_str(), uniformStr.data(),
                             attributeStr.c_str(), source};
  if (param == GetGLShaderType(ShaderType::FRAGMENT)) {
    sources[3] = "\n";
  }
  glShaderSource(id, sizeof(sources) / sizeof(*sources), sources, nullptr);
  glCompileShader(id);
  CheckShaderCompileError(id);
}

GLuint ProgramState::CreateShader(ShaderType type) {
  GLuint shader = glCreateShader(GetGLShaderType(type));
  _shaderId.push_back(shader);
  return shader;
}

#define CHECK_LOC(x)                                                                     \
  if (x < 0)                                                                             \
    return;

#define GET_CHECK_LOC(x, name)                                                           \
  auto x = GetUniformLocation(name);                                                     \
  CHECK_LOC(x)

void ProgramState::ProgramState::SetUniformFloat(const std::string &name, GLfloat val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1f(_id, loc, val);
}

void ProgramState::SetUniformFloatArray(const std::string &name, GLfloat *val,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1fv(_id, loc, count, val);
}

void ProgramState::SetUniformDouble(const std::string &name, GLdouble val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1d(_id, loc, val);
}

void ProgramState::SetUniformDoubleArray(const std::string &name, GLdouble *val,
                                         GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1dv(_id, loc, count, val);
}

void ProgramState::SetUniformFloat2(const std::string &name, glm::vec2 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2f(_id, loc, val.x, val.y);
}

void ProgramState::SetUniformFloat2Array(const std::string &name, glm::vec2 *val,
                                         GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2fv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformDouble2(const std::string &name, glm::dvec2 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2d(_id, loc, val.x, val.y);
}

void ProgramState::SetUniformDouble2Array(const std::string &name, glm::dvec2 *val,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2dv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformDouble3Array(const std::string &name, glm::dvec3 *val,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3dv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformDouble3(const std::string &name, glm::dvec3 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3d(_id, loc, val.x, val.y, val.z);
}

void ProgramState::SetUniformFloat4(const std::string &name, glm::vec4 val) {
  GET_CHECK_LOC(loc, name);
  glProgramUniform4f(_id, loc, val.x, val.y, val.z, val.w);
}

void ProgramState::SetUniformFloat4Array(const std::string &name, glm::vec4 *val,
                                         GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform4fv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformInt(const std::string &name, GLint val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1i(_id, loc, val);
}
void ProgramState::SetUniformIntArray(const std::string &name, GLint *val,
                                      GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1iv(_id, loc, count, val);
}
void ProgramState::SetUniformInt2(const std::string &name, glm::ivec2 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2i(_id, loc, val.x, val.y);
}
void ProgramState::SetUniformInt2Array(const std::string &name, glm::ivec2 *val,
                                       GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2iv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformInt3(const std::string &name, glm::ivec3 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3i(_id, loc, val.x, val.y, val.z);
}
void ProgramState::SetUniformInt3Array(const std::string &name, glm::ivec3 *val,
                                       GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3iv(_id, loc, count, glm::value_ptr(val[0]));
}
void ProgramState::SetUniformInt4(const std::string &name, glm::ivec4 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform4i(_id, loc, val.x, val.y, val.z, val.w);
}
void ProgramState::SetUniformInt4Array(const std::string &name, glm::ivec4 *val,
                                       GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform4iv(_id, loc, count, glm::value_ptr(val[0]));
}

// unsigned int uniform
void ProgramState::SetUniformUInt(const std::string &name, GLuint val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1ui(_id, loc, val);
}
void ProgramState::SetUniformUIntArray(const std::string &name, GLuint *val,
                                       GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform1uiv(_id, loc, count, val);
}
void ProgramState::SetUniformUInt2(const std::string &name, glm::uvec2 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2ui(_id, loc, val.x, val.y);
}
void ProgramState::SetUniformUInt2Array(const std::string &name, glm::uvec2 *val,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform2uiv(_id, loc, count, glm::value_ptr(val[0]));
}
void ProgramState::SetUniformUInt3(const std::string &name, glm::uvec3 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3ui(_id, loc, val.x, val.y, val.z);
}
void ProgramState::SetUniformUInt3Array(const std::string &name, glm::uvec3 *val,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform3uiv(_id, loc, count, glm::value_ptr(val[0]));
}
void ProgramState::SetUniformUInt4(const std::string &name, glm::uvec4 val) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform4ui(_id, loc, val.x, val.y, val.z, val.w);
}
void ProgramState::SetUniformUInt4Array(const std::string &name, glm::uvec4 *val,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniform4uiv(_id, loc, count, glm::value_ptr(val[0]));
}

void ProgramState::SetUniformMat2fArray(const std::string &name, const glm::mat2 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat2dArray(const std::string &name, const glm::dmat2 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat3fArray(const std::string &name, const glm::mat3 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat2dArray(const std::string &name, glm::dmat3 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4fArray(const std::string &name, const glm::mat4 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4dArray(const std::string &name, glm::dmat4 *mat,
                                        GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}

void ProgramState::SetUniformMat2x3fArray(const std::string &name, const glm::mat2x3 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2x3fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat2x3dArray(const std::string &name,
                                          const glm::dmat2x3 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2x3dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat2x4fArray(const std::string &name, const glm::mat2x4 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2x4fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat2x4dArray(const std::string &name,
                                          const glm::dmat2x4 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix2x4dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat3x4fArray(const std::string &name, const glm::mat3x4 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3x4fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat3x4dArray(const std::string &name,
                                          const glm::dmat3x4 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3x4dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat3x2fArray(const std::string &name, const glm::mat3x2 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3x2fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat3x2dArray(const std::string &name,
                                          const glm::dmat3x2 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix3x2dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4x2fArray(const std::string &name, const glm::mat4x2 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4x2fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4x2dArray(const std::string &name,
                                          const glm::dmat4x2 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4x2dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4x3fArray(const std::string &name, const glm::mat4x3 *mat,
                                          GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4x3fv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
void ProgramState::SetUniformMat4x3dArray(const std::string &name,
                                          const glm::dmat4x3 *mat, GLsizei count) {
  GET_CHECK_LOC(loc, name)
  glProgramUniformMatrix4x3dv(_id, loc, count, GL_FALSE, glm::value_ptr(mat[0]));
}
#undef CHECK_LOC
#undef GET_CHECK_LOC

} // namespace Program